查看原文
其他

张量 101

王圣元 王的机器 2019-05-25



此文献给今天五岁的女儿四岁献上的是至今最满意的胶囊网络


0引言


斯蒂文想用聚宽 (JoinQuant) 写一个股票投资的量化策略,首先需要收集大量的数据。(下面聚宽代码基本上可以一看就懂,详细的代码解释见本帖第四章)


阶段一

斯蒂文查了查 2019 年 1 月 3 日平安银行 (000001.XSHE) 的收盘价,发现是 9.28,他默默将这个单数字存到 X0 里。


X0 = get_price( '000001.XSHE',
start_date='2019-01-03',
end_date='2019-01-03',
frequency='daily',
fields=['close'] )



X0 又称为标量 (scalar),或更严谨的称为 0 维张量 (0D tensor)。


阶段二

单天股票价格太少,至少要算一些均值、标准差这些统计量吧。


斯蒂文从一天的数据扩展到一年,下载了从 2019 年 1 月 3 日起过去一年的平安银行历史收盘价,存到 X1 里。


X1 = get_price( '000001.XSHE',
start_date='2018-01-03',
end_date='2019-01-03',
frequency='daily',
fields=['close'] )



X1 在 X0 基础上添加了时间维度 (红色箭头),从标量扩展成向量 (vector),又称为 1 维张量 (1D tensor)。


阶段三

单个股票太少,分散原则告诉我们需要投资相关性系数为负的两支股票。


斯蒂文增加了茅台股票 (600519.XSHG),下载了从 2019年 1 月 3 日起过去一年的平安银行和茅台历史收盘价,存到 X2 里。


X2 = get_price( ['000001.XSHE','600519.XSHG'],
start_date='2018-01-03',
end_date='2019-01-03',
frequency='daily',
fields=['close'] )


X2 在 X1 基础上添加了横截维度 (蓝色箭头),从向量扩展成矩阵 (matrix),又称为 2 维张量 (2D tensor)。


阶段四

收盘价一个信息不够,在趋势追踪模型中,价格和交易量是在股票走势中相当重要的因素。


斯蒂文增加了交易量,下载了从 2019 年 1 月 3 日起过去一年的平安银行和茅台历史收盘价和交易量,存到 X3 里。


X3 = get_price( ['000001.XSHE','600519.XSHG'],
start_date='2018-01-03',
end_date='2019-01-03',
frequency='daily',
fields=['close','volume'] )



X3 在 X2 基础上添加了信息维度 (绿色箭头),从矩阵扩展成 3 维张量 (3D tensor)。

 

阶段五

收盘信息太过于少,如果要日内交易怎么办?


斯蒂文又增加了 tick 数据 (聚宽对股票的 tick 数据切片时间为 3 秒),下载了从 2019 年 1 月 3 日起过去一年的平安银行和茅台历史 tick 价格和交易量,存到 X4 里。


import pandas as pd
tick_data1 = []
tick_data2 = []

for date in pd.date_range('2018-01-04', '2019-01-04').tolist():

   d = get_ticks('000001.XSHE',
 start_dt=None,
 end_dt=date,
 count=5,
 fields=['time','current','volume'])
   tick_data1.append(d)

   d = get_ticks('600519.XSHG',
 start_dt=None,
 end_dt=date,
 count=5,
fields=['time','current','volume'])
   tick_data2.append(d)



X4 在 X3 基础上添加了频率维度(灰色箭头),从 3 维张量扩展成 4 维张量 (4D tensor)。


纵横时间之长,驰骋股票之宽,领略信息之深,体会频率之快,如果能够获取下图这四维数据,再做不好量化策略就。。。等等,这帖不是打深度学习的基础吗,怎么扯到量化投资了?其实张量无处不在,因为数据无处不在,维度无处不在。



本来一开始是想讲张量求导 (tensor derivative) 和计算图 (computational graph) 的,但讲这个必须把张量要讲清楚吧。后来解释张量一发不可收拾写成了完整一贴。这贴把张量基础打牢,下贴继续张量求导和计算图。



本帖目录如下:

目录

第一章 - 线性代数的张量


第二章 - 编程语言的张量


    2.1 Python

    2.2 TensorFlow

    2.3 MXNet

    2.4 PyTorch

    2.5 Matlab


第三章 - 机器学习的张量


    3.1 简介

    3.2 2D 数据表

    3.3 3D 序列数据

    3.4 4D 图像数据

    3.5 5D 视屏数据


第四章 - 量化金融的张量


    4.1 简介

    4.2 JoinQuant 之 A 股

    4.3 Quantopian 之美股

第五章 - 张量运算

    5.1 化繁为简

    5.2 由简推繁


总结



1线性代数的张量


线性代数大家肯定学过标量、向量和矩阵,它们分别称为 0 维张量、1 维张量和 2 维张量,而高于 2 维的张量统称为 n 维张量 (n ≥ 3)。




2编程语言的向量


很多编程语言把张量当成数据的容器 (container)。在计算机中最终处理的都是数值型数据,因此张量大多指一个装着数值类变量的容器。接下来我们看看 Python, TensorFlow, MXNet, PyTorch 和 Matlab 里面的张量长什么样把。


2.1

Python


深度学习框架 Keras 就直接用 Python 的 numpy 的模块来使用张量的。上面也讲过,张量就是多维数组,不像 Keras 直接用 Python 的 numpy,其他深度学习框架对张量或多维数组稍微做了些改变,比如:


  • Tensorflow 里用 tf.Tensor

  • MXNet 里用 ndarray

  • PyTorch 里用 torch.tensor


首先从 Python 里面导入 numpy 模块。这里的 np 是 numpy 的缩写形式。


import numpy as np


下面是用 Python 的 numpy 来定义 到 维的张量。


# 0维张量
X0 = np.array(42)

# 1维张量        
X1 = np.array( [1 2] )

# 2维张量    
X2 = np.array( [1 2], [3 4] )

# 3维张量  
X3 = np.array([[[147], [258], [369]],
              [[123], [456], [789]],
              [[987], [654], [321]]])  

# 4维张量
X4 = np.ones( (6000028283) )


不难看出


  • X0, X1, X2, X3 都是用 np.array 直接设定张量里的元素来定义张量

  • X4 用 np.ones 和张量的形状 (60000,28, 28, 3) 来定义一个所有元素都是 的张量


2.2

TensorFlow


顾名思义,TensorFlow 建立了计算图,让张量 (tensor) 从中流动 (flow),因此取名 TensorFlow。首先从 tensorflow 里面导入 tf.Tensor 模块。这里的 tf 是 tensorflow 的缩写形式。


import tensorflow as tf


tf.Tensor 由以下两个特征定义:

 

  1. 数据类型 (data type),包括整数、浮点数和字符等

  2. 形状 (shape)


Tensor 中的每个元素都具有相同而其已知的数据类型,形状是张量的每个维度 (TensorFlow 称 rank) 上的元素个数,而且可能只是部分已知。比如一张彩色照片由宽 28,高 28,3 色道的元素组成,那么该三维照片数据的形状 (完全可知) 可写成


    [28, 28, 3]


将照片当做神经网络的输入时,有时候我们不清楚有多少张照片,因此整个四维照片数据集的形状 (部分已知可写成


    [None, 28, 28, 3]


其中 None 指照片数目未知。


TensorFlow 官网这样解释 rank 和 shape 的。



tf.Tensor 还可再细分成 tf.Variable, tf.constant 和 tf.placeholder 等,它们意思一眼可知,分别代表变量、常量和占位符的意思。下面是用 tf.Variable 来定义 0 到 4 维的张量。


# 0维张量
X0 = tf.Variable( 1031, tf.int16 )

# 1维张量
X1 = tf.Variable( [3.141592.71828], tf.float32 )

# 2维张量
X2 = tf.Variable( [ [49], [1625] ], tf.int32 )

# 3维张量
X3 = tf.ones( [600002828] )

# 4维张量
X4 = tf.zeros( [None28283] )


不难看出


  • 上面 X0, X1, X2 都是用 tf. Variable 直接设定张量里的元素来定义张量

  • X3 用 tf.ones 和张量的形状 (60000, 28, 28) 来定义一个所有元素都是 1 的张量

  • X4 用 tf.zeros 和张量的形状 (None, 28, 28, 3) 来定义一个所有元素都是 0 的张量。


X3 像不像一个数目已知的黑白照片的数据集?X4 像不像一个数目未知的照片的数据集?


2.3

MXNet


首先从 mxnet ⼊ ndarray 模块。这里的 nd 是 ndarray 的缩写形式。


from mxnet import nd


下面是用 mxnet 的 ndarray 来定义 到 维的张量。


# 0维张量
X0 = 5

# 1维张量
X1 = nd.arange(12)

# 2维张量
X2 = X1.reshape( (34) )

# 3维张量
X3 = nd.random.normal( 01, shape=(345) )

# 4维张量
X4 = nd.zeros( (1000028283) )


不难看出

 

  • X0, X1 都是用 nd 直接设定张量里的元素来定义张量

  • X2 将一维张量 X1 重新排成 3×4 的二维张量

  • X3 用 nd.random.normal 和张量的形状 (3, 4, 5) 来定义一个高斯随机张量

  • X4 用 nd.zeros 和张量的形状 (10000, 28, 28, 3) 来定义一个所有元素都是 的张量


2.4

PyTorch

 

首先从 PyTorch ⼊ torch 模块。


import torch


下面是用 PyTorch 的 torch 来定义 到 维的张量。


# 0维张量
X0 = torch.tensor(3)

# 1维张量
X1 = torch.tensor([9-9])

# 2维张量
X2 = torch.rand(55)

# 3维张量
X3 = torch.tensor([[[3]]])

# 4维张量
X4 = torch.tensor.new_ones((6000028283))


不难看出


  • X0, X1, X3 都是用 torch.tensor 直接设定张量里的元素来定义张量

  • X2 用 torch.rand 和张量的形状 (5, 5) 来定义一个随机张量

  • X4 用 new_ones 和张量的形状 (60000, 28, 28, 3) 来定义一个所有元素都是 的张量


2.5

Matlab


下面是用 Matlab 来定义 到 维的张量。


% 0 维数组
X0 = 2;

% 1 维数组
X1 = [1 2; 3 4];

% 2 维数组
X2 = [1 2 3; 4 5 6; 7 8 9];

% 3 维数组
X3 = cat( 3, X2, [3 2 1; 0 9 8; 5 3 7] );

% 4 维数组
X4 = repmat( 5,[2 3 1 4] );


不难看出


  • X0, X1, X2 都是用方括号 “[] 直接设定张量里的元素来定义张量

  • X3 用 cat 把 X2 (3×3) 和另一个 3×3 按张量按第 维度上拼接起来

  • X4 用 repmat 把元素 复制一个形状为 [2, 3, 1, 4] 的张量



3机器学习的张量


3.1

简介

机器学习 (深度学习中用到的数据,包括结构性数据 (数据表、序列和非结构性数据 (图片、视屏都是张量,总结如下:


  • 数据表:2 维,形状 = (样本数,特征数)

  • 序列类:3 维,形状 = (样本数,步长,特征数)

  • 图像类:4 维,形状 = (样本数,宽,高,通道数)

  • 视屏类:5 维,形状 = (样本数,帧数,宽,高,通道数)


机器学习,尤其深度学习,需要大量的数据,因此样本数肯定占一个维度,惯例我们把它称为维度 1。这样机器学习要处理的张量至少从 维开始。


3.2

2D 数据表


维张量就是矩阵,也叫数据表,一般用 csv 存储。



这套房屋 21,000 个数据包括其价格 (y),平方英尺,卧室数,楼层,日期,翻新年份等等 21 栏。该数据形状为 (21000, 21)。传统机器学习的线性回归可以来预测房价。


维张量的数据表示图如下:



3.3

3D 序列数据


推特 (twitter) 的每条推文 (tweet) 规定只能发 280 个字符。在编码推文时,将 280 个字符的序列用独热编码 (one-hot encoding) 到包含 128 个字符的 ASCII 表,如下所示。



这样,每条推文都可以编码为 维张量形状 (280, 128),比如一条 tweet 是 “I love python :)”,这句话映射到 ASCII 表变成:



如果收集到 百万条推文,那么整个数据集的形状为 (1000000, 280, 128)。传统机器学习的对率回归可以来做情感分析。


维张量的数据表示图如下:



3.4

4D 图像数据


图像通常具有3个维度:宽度,高度和颜色通道。虽然是黑白图像 (如 MNIST 数字) 只有一个颜色通道,按照惯例,我们还是把它当成 维,即颜色通道只有一维。


  • 一组黑白照片可存成形状为 (样本数,宽,高,1) 的 维张量

  • 一组彩色照片可存成形状为 (样本数,宽,高,3) 的 维张量


通常 代表黑色,255 代表白色。


4 维张量的数据表示图如下:



3.5

5D 视屏数据


视频可以被分解成一幅幅帧 (frame)


  • 每幅帧就是彩色图像,可以存储在形状是 (宽度,高度,通道) 的 3D 张量中

  • 视屏 (一个序列的帧) 可以存储在形状是 (帧数,宽度,高度,通道) 的 4D 张量中

  • 一批不同的视频可以存储在形状是 (样本数,帧数,宽度,高度,通道) 的 5D 张量中


下面一个 9:42 秒的 1280x720 YouTube 视频 (哈登三分绝杀勇士),被分解成 40 个样本数据,每个样本包括 240 帧。这样的视频剪辑将存储在形状为 (40, 240, 1280, 720, 3) 的张量中。



5 维张量的数据表示图如下:




4量化金融的张量


4.1

简介


在量化金融中,我们用股票数据举例来说明不同维度的张量,习惯将维度定义如下:



结合上表,下图清晰画出各个维度的代表的意思。



接下来我们特别选取聚宽 (JoinQuant) 量化平台来举 股的例子,选取 Quantoptian 量化平台来举美股的例子。


4.2

JoinQuant 之 A 股


首先引入 Python 基本的 numpy 模块。


import numpy as np
0 维张量:一个收盘价
# 茅台 (600519.XSHG) 在2019年1月3日的收盘价
X0 = get_price('600519.XSHG',start_date='2019-01-03',
end_date='2019-01-03', frequency='daily'
fields='close')

print( '张量形状是', X0.shape )
print( '张量压缩后形状是', np.squeeze(X0).shape )
X0


结果得到茅台在 2019 年 月 日的收盘价,记为 X0,它是一个 1×1 的标量,压缩之后的形状是 (),是一个 维张量。



1 维张量:加入时间维度
# 获取茅台 (600519.XSHG) 从2019年1月3日起的4天历史收盘价
X1 = get_price('600519.XSHG', start_date='2018-12-27'
end_date='2019-01-03', frequency='daily'
fields='close')

print( '张量形状是', X1.shape )
print( '张量压缩后形状是', np.squeeze(X1).shape )
X1


结果得到茅台从 2019 年 月 日起 天的历史收盘价,记为 X1,它是一个 4×1 的向量,压缩之后的形状是 (4,),是一个 维张量。



2 维张量:加入股票维度
# 获取茅台 (600519.XSHG) 和平安银行 (000001.XSHE) 
# 从2019年1月3日起的4天历史收盘价

X2 = get_price(['600519.XSHG''000001.XSHE'], 
start_date='2018-12-27', end_date='2019-01-03',
frequency='daily', fields=['close'])

print( '张量形状是', X2.shape )
print( '张量压缩后形状是', np.squeeze(X2).shape )

X2['close']


结果得到茅台和平安银行从 2019 年 月 日起 天的历史收盘价,记为 X2,它是一个 1×4×2 的张量,这个 维度指的产出只有 ['close'一种类型的信息,因此该维度可以被压缩。X2 压缩之后的形状是 (4, 2),是一个 维张量。



3 维张量:加入信息维度
# 获取茅台 (600519.XSHG) 和平安银行 (000001.XSHE)
# 从2019年1月3日起的4天历史收盘价和交易量
X3 = get_price(['600519.XSHG''000001.XSHE'],
start_date='2018-12-27', end_date='2019-01-03',
frequency='daily', fields=['close','volume'])

print( '张量形状是', X3.shape )
print( '张量压缩后形状是', np.squeeze(X3).shape )

X3['close']
X3['volume']


结果得到茅台和平安银行从 2019 年 月 日起 天的历史收盘价和交易量,记为 X3,它是一个 2×4×2 的张量,这个 维度指的产出有 ['close'和 ['volume'两种类型的信息,因此该维度不能被压缩。X3 压缩之后的形状还是 (2, 4, 2),是一个 维张量。



4 维张量:加入频率维度

当你可以在时间维度上纵横 (不同天,如 t, t-1, t-2, …),可以在横截维度上驰骋 (不同股票,如茅台、平安等),可以在信息维度上选择 (不同产出,如收盘价、交易量等),你几乎可以获取任何信息。但是还有一个维度,频率维度,即在日内收集 tick 级别的数据。国外和国内对于 tick 数据定义有些不同:


  • 国外:任何委托单 (order) 使委托账本 (order book) 变化而得到的表格

  • 国内:对委托账本的按一定切片时间 (500 毫秒,秒,秒等抽样的信息。


# 引入 tick 级别的当前价和交易量
# 融入 3 维张量得到的张量形状为 (2, 4, 2, 10)
d = get_ticks('000001.XSHE', start_dt=None
end_dt="2019-01-03", count=10,
fields=['time','current','volume'])

d


上面代码是获取平安银行在 2019 年 月 日开盘前,即 2019 年 月 日收盘前 (A 股是每天下午 15 点收盘的 10 个 tick 数据 (现价和交易量),得到以下结果是个 record 类型的数组。该数组有 10 条记录,每条记录分别包含时间、现价和交易量。



从上面结果来看,时间用了科学计数表现形式,根本看不出来具体的精确到秒的区别,因此我们将 record 类型数组转换成 list


d.tolist()



现在时间看清楚了,结合 维张量里的收盘价和交易量,我们发现 tick 数据最后一行 (时间是 2019 年 月 日 15 点的现价 9.19 和交易量 539386 和下图右的同一天的平安银行栏下的数据完全吻合。



此外我们发现时间栏下面的数相差为 3,即 tick 大小为 秒,即交易所每 秒照一次快照。


4.3

Quantopian 之美股


首先引入 Python 基本的 numpy 和 pandas 模块,还有 quantopian.research 特有的 prices 和 symbols 模块。


from quantopian.research import prices, symbols
import numpy as np
import pandas as pd
0 维张量:一个收盘价
# 苹果 (AAPL) 在2019年1月3日的收盘价
X0 = prices( assets=symbols(['AAPL']), 
start='2019-01-03', end='2019-01-03' )

print 'The shape of tenor is', X0.shape
print 'The shape of tenor (after squeeze) is'
np.squeeze(X0).shape

X0


结果得到苹果在 2019 年 月 日的收盘价,记为 X0,它是一个 1×1 的标量,压缩之后的形状是 (),是一个 维张量。



1 维张量:加入时间维度
# 苹果 (AAPL) 从2019年1月3日起的5天历史收盘价
X1 = prices( assets=symbols(['AAPL']), 
start='2018-12-27', end='2019-01-03' )

print 'The shape of tenor is', X1.shape
print 'The shape of tenor (after squeeze) is', np.squeeze(X1).shape

X1


结果得到苹果从 2019 年 月 日起 5 天的历史收盘价,记为 X1,它是一个 5×1 的向量,压缩之后的形状是 (5,),是一个 维张量。



2 维张量:加入股票维度
# 苹果 (AAPL) 和脸书 (FB) 
# 从2019年1月3日起的5天历史收盘价

X2 = prices( assets=symbols(['AAPL','FB']), 
start='2018-12-27', end='2019-01-03' )

print 'The shape of tenor is', X2.shape
print 'The shape of tenor (after squeeze) is', np.squeeze(X2).shape

X2


结果得到苹果和脸书从 2019 年 月 日起 天的历史收盘价,记为 X2,它是一个 5×2 的张量。X2 压缩之后的形状还是 (5, 2),是一个 维张量。



3 维张量:加入信息维度

在 Quantopian 框架中,prices 和 volume 是分开的物体,因此不能够像聚宽那样用 get_price 函数将这两个信息一次性获取(只需设定 fields =['close','volume'])。要在 Quantopian 达到同样效果,必须使用自带的 PipelinePipeline 中文是管道的意思,在这里指的是贯穿了整个数据系统的一个管道,使得使用者能够集中精力从数据中获取所需要的信息,而不是把精力花费在管理日常数据和管理数据库方面。通俗来说,Pipeline 就是自动化数据工作的利器。

 

首先引入下面四个模块,前两个用来创建和运行PipelineUSEquityPricing 储存了美股的各种信息,StaticAssets 提供了一组固定资产代码而用于调试 Pipeline 的功能 (后面解释)


from quantopian.pipeline import Pipeline
from quantopian.research import run_pipeline
from quantopian.pipeline.data import USEquityPricing
from quantopian.pipeline.filters import StaticAssets


要想用 Pipeline,必先建 Pipeline。以下代码创建一个数据表 (dataframe),限定关注的信息是收盘价和交易量,限定关注的股票是苹果和脸书。


def make_pipeline():
   
   close_price = USEquityPricing.close.latest
   volume = USEquityPricing.volume.latest
   
   return Pipeline(
       columns = {
           'close': close_price,
           'volume': volume,
       },
       screen = StaticAssets(symbols(['AAPL','FB'])),
   )


建完 Pipeline,就可运行。用 run_pipeline 函数加上对日期的限定就可以了。


X3 = run_pipeline(
   make_pipeline(),
   start_date='2018-12-27',
   end_date='2019-01-03',
)

print 'The shape of tenor is', X3.shape
print 'The shape of tenor (after squeeze) is', np.squeeze(X3).shape

X3


结果得到苹果和脸书从 2019 年 月 日起 天的历史收盘价和交易量,记为 X3,它是一个 10×2 的张量。咋一看它不是 维而是 维张量,但是从下面数据表的结构可看出它是一个 MultiIndex 的表。在行上有两层,第一层是时间层,第二层是股票层,而列是信息层。表面上看 X3 是个 维张量,实质上它是个 维张量。



4 维张量:加入频率维度

找了半天,好像 Quantoptian 不支持 tick 数据的获取。这点要给聚宽点个赞。



5张量运算


5.1

化繁为简


深度学习中的神经网络本质就是张量运算,本节用最简单的神经网络 (没有隐藏层) 来列举所有类型的张量运算。



上图实际上是用神经网络来识别手写数字 (MNIST 的数据),大概分四个步骤: 


  1. 提取黑白图像的像素矩阵,重塑成向量 X

  2. 用权重矩阵 点乘 X

  3. 加上偏置向量 b

  4. 将分数向量 WX + b 用 softmax 转换成概率向量


对应的概率值最大的位置索引便是图像里的数字,上图数字 对应的概率为 0.97,因此推断图片里是 9


将上述四步,从输入到输出用一个公式表示:



复杂的公式里面涉及到四类张量运算,从里到外按顺序来看:


  1. 重塑形状 (reshape)

  2.  张量点乘 (tensor dot)

  3. 广播机制 (boardcasting)

  4. 元素层面 (element-wise)


下面我们来各个击破。

重塑形状

重塑张量的形状意味着重新排列各个维度的元素个数以匹配目标形状。重塑形成的张量和初始张量有同样的元素。


再看三个简单例子。


例一:生成一个 3×2 的矩阵,该矩阵里有 个元素。


x = np.array( [[0, 1], [2, 3], [4, 5]] )
print(x.shape)
x
(3, 2)

array([[0, 1],
      [2, 3],
      [4, 5]])


例二:重塑成 6×1 的矩阵,也是 个元素,因此使用 reshape 不会报错。需要注意的是在 python 里是按行来获取元素来排列的 (Matlab 是按列来获取)


x = x.reshape( 6, 1 )
print(x.shape)
x
(6, 1)

array([[0],
      [1],
      [2],
      [3],
      [4],
      [5]])


例三:重塑成 2×3 的矩阵,也是 个元素,因此使用 reshape 不会报错。这里在 reshape 函数的第二个参数放的是 -1,意思就是我不知道或者不想费力来设定这一维度的元素个数,python 来帮我算出,结果也看到了是 3。因此 reshape(2, -1) 和 reshape(2, 3) 是等价的,只不过使用起来更方便。


x = x.reshape( 2, -1 )
print(x.shape)
x
(2, 3)

array([[0, 1, 2],
      [3, 4, 5]])


张量点乘

在 python 中乘法指的是在元素层面做乘法,用符号 “*”。在 numpy 中,点乘指的不是在元素层面做乘法,用 np.dot 函数。点乘左右两边最常见的张量就是

 

  • 向量 (1D) 和向量 (1D)

  • 矩阵 (2D) 和向量 (1D)

  • 矩阵 (2D) 和矩阵 (2D)


分别看看三个简单例子。


例一np.dot(向量, 向量) 实际上做的就是内积,即把两个向量每个元素相乘,最后再加总。点乘结果 10 是个标量 (0D 张量),形状 = ()


x = np.array( [1, 2, 3] )
y = np.array( [3, 2, 1] )
z = np.dot(x,y)
print(z.shape)
z
()
10


例二np.dot(矩阵, 向量) 实际上做的就是普通的矩阵乘以向量。点乘结果是个向量 (1D 张量),形状 = (2, )


x = np.array( [1, 2, 3] )
y = np.array( [[3, 2, 1], [1, 1, 1]] )
z = np.dot(y,x)
print(z.shape)
z
(2,)
array([10,  6])


例三np.dot(矩阵, 矩阵) 实际上做的就是普通的矩阵乘以矩阵。点乘结果是个矩阵 (2D 张量),形状 = (2, 3)


x = np.array( [[1, 2, 3], [1, 2, 3], [1, 2, 3]] )
y = np.array( [[3, 2, 1], [1, 1, 1]] )
z = np.dot(y,x)
print(z.shape)
z
(2, 3)
array([[ 6, 12, 18],
      [ 3,  6,  9]])

从例二和例三看出,当 x 第二个维度的元素 (x.shape[1]) 和 y 第一个维度的元素 (y.shape[0]) 个数相等时,np.dot(X, Y) 才有意义,点乘得到的结果形状= (X.shape[0], y.shape[1])。


上面例子都是低维张量 (维度 ≤ 2) 的点乘运算,接下来我们看两个稍微复杂的例子。


例四:当 是 3D 张量,是 1D 张量,np.dot(x, y) 是将 和 最后一维的元素相乘并加总。此例 的形状是 (2, 3, 4)的形状是 (4, ),因此点乘结果的形状是 (2, 3)


x = np.ones( shape=(2, 3, 4) )
y = np.array( [1, 2, 3, 4] )
z = np.dot(x,y)
print(z.shape)
z
(2, 3)
array([[10., 10., 10.],
      [10., 10., 10.]])


例五:当 是 3D 张量,是 2D 张量,np.dot(x, y) 是将 最后一维和 倒数第二维的元素相乘并加总。此例 的形状是 (2, 3, 4)的形状是 (4, 2),因此点乘结果的形状是 (2, 3, 2)


x = np.random.normal( 0, 1, size=(2, 3, 4) )
y = np.random.normal( 0, 1, size=(4, 2) )
z = np.dot(x,y)
print(z.shape)
z
(2, 3, 2)
array([[[ 2.11753451, -0.27546168],
       [-1.23348676,  0.42524653],
       [-4.349676  , -0.3030879 ]],

      [[ 0.15537744,  0.44865273],
       [-3.09328194, -0.43473885],
       [ 0.27844225, -0.48024693]]])


例五的规则也适用于 nD 张量和 mD 张量 (当 m ≥ 2 的点乘


广播机制

当对两个形状不同的张量按元素操作时,可能会触发广播机制。具体做法,先适当复制元素使得这两个张量形状相同后再按元素操作,两个步骤:


  1. 广播轴 (broadcast axes):比对两个张量的维度,将形状小的张量的维度 () 补齐

  2. 复制元素:顺着补齐的轴,将形状小的张量里的元素复制,使得最终形状和另一个张量吻合



用代码验证一下我们的理解。


x = np.arange(1,7).reshape(3,2)
y = np.arange(1,3).reshape(2)
print( (x + y).shape )
x + y
(3, 2)
array([[2, 4],
      [4, 6],
      [6, 8]])


有一个情况比较特殊,就是 和 y 都是 2D 张量 (形状一样),但是 x 和 y 分别在不同维度的元素个数为 1。这样的话广播机制也适用于它们,如下图。



用代码验证一下我们的理解。

x = np.arange(1,4).reshape(3,1)
y = np.arange(1,3).reshape(1,2)
print( (x + y).shape )
x + y
(3, 2)
array([[2, 3],
      [3, 4],
      [4, 5]])


扩展到高维张量,


  • 一个形状是 (n0, …, ni, ni+1, …, nm)

  • 一个形状是 (ni+1, …, nm)


在进行张量运算时,对于轴 n到 ni,广播机制将自动发生。


用下面两个例子来验证一下我们的理解。


x = np.arange(1,25).reshape(2,3,4)
y = np.arange(1,5).reshape(4)
print( (x + y).shape )
x + y
(2, 3, 4)
array([[[ 2,  4,  6,  8],
       [ 6,  8, 10, 12],
       [10, 12, 14, 16]],

      [[14, 16, 18, 20],
       [18, 20, 22, 24],
       [22, 24, 26, 28]]])


x = np.arange(1,25).reshape(2,3,4)
y = np.arange(1,13).reshape(3,4)
print( (x + y).shape )
x + y
(2, 3, 4)
array([[[ 2,  4,  6,  8],
       [10, 12, 14, 16],
       [18, 20, 22, 24]],

      [[14, 16, 18, 20],
       [22, 24, 26, 28],
       [30, 32, 34, 36]]])

元素层面

在元素层面的操作用两类:


  1. 用运算符 “+,–, *, /” 来连接两个形状一样的张量 (要不然触发广播机制)

  2. 用函数如 exp(), softmax() 来传递一个张量


两类在元素层面运算出来的的结果张量的形状不变


举个简单例子,首先生成两个随机变量,形状 = (2, 3)


x = np.random.normal( 0, 1, size=(2,3) )
y = np.random.normal( 0, 1, size=(2,3) )


按元素加法:

x + y
array([[ 0.90289729,  1.09982878, -2.91277851],
      [ 0.42207416,  1.37044143, -0.12210443]])


按元素减法:

x - y
array([[-0.90005841,  0.41618533,  0.86788043],
      [-0.18896878,  1.22037828,  0.00227839]])


按元素乘法:

x * y
array([[1.27959426e-03, 2.59103276e-01, 1.93276555e+00],
      [3.56093497e-02, 9.71966398e-02, 3.72607504e-03]])


按元素除法:

x / y
array([[1.57457067e-03, 2.21755087e+00, 5.40884040e-01],
      [3.81487730e-01, 1.72648632e+01, 9.63364914e-01]])


函数 exp()

np.exp(x)
array([[1.00142045, 2.134019  , 0.35971291],
      [1.12361671, 3.65249266, 0.94184645]])


函数 softmax()

def softmax(x, axis=-1):
   e_x = np.exp(x - np.max(x,axis,keepdims=True))
   return e_x / e_x.sum(axis,keepdims=True)


代码对应着下面流程图秒懂。在编写 softmax 时需要注意的是,当 s 太大或太小,分母可能无穷大或者是零,这样会造成上溢 (overflow) 和下溢 (underflow) 问题。一个聪明的办法是找到 s 的最大值,然后分别减去它。这样最后的结果不会受影响,而且排除了上溢和下溢的可能性。



是个矩阵,由上图可知 softmax 通常是作用在向量上,这样用 softmax() 函数时必须设定 axis,即在哪个轴 (也叫维度) 上的元素做 softmax,比如 axis =0 指第一维,axis = 1 指第二维,axis= -1 指最后一维。

y = softmax( x, axis=0 )
print(y)
np.sum( y, axis=0 )
array([[0.47124844, 0.36879196, 0.27637073],
      [0.52875156, 0.63120804, 0.72362927]])

array([1., 1., 1.])


在行上元素做 softmax,显然在行上元素求和都等于  1,因为有三列,所有最后结果是三个 1


y = softmax( x, axis=1 )
print(y)
np.sum( y, axis=1 )
array([[0.28651697, 0.61056537, 0.10291766],
      [0.19650671, 0.63877595, 0.16471734]])

array([1., 1.])


在列上元素做 softmax,显然在列上元素求和都等于 1,因为有两行,所有最后结果是两个 1


5.2

由简推繁


上节已经弄懂四种张量运算的类型了,本节再回到用神经网络来识别数字的例子。先不用管权重 和偏置 如何优化出来的,假设已经有了最优 和 b,我们主要是想验证一下在实际问题中,张量运算是如何进行的。


我们用 TensorFlow 和 Keras 来展示,数据集是 MNIST。首先导入numpy, tensorflow, keras 等模块。


import numpy as np
import tensorflow as tf
import tensorflow.keras as keras
from tensorflow.keras.datasets import mnist


定义 MINST 里面黑白数字图片的宽度和高度都为 28,通道为 1 (不是彩色)。定义最终输出的个数为 10,因为只有 10 个数字。


# input image dimensions
n_W, n_H = 28, 28
# class dimensions
n_y = 10


为了模仿真实的机器学习流程,将数据分成训练集和测试集。但本节重点只看张量运算,因此我们只关注训练集,发现它的形状是 (60000, 28, 28),其中 60000 代表图片个数。


(x_train, y_train), (x_test, y_test)
= mnist.load_data()

x_train.shape
(60000, 28, 28)


回顾本章开始列出的公式,来看看如何从“60000 张图片输入 X_train”经过一系列的张量运算得到“60000 个概率输出向量”,顺带也看看每次运算之后向量的形状如何变化。起点从 X_train 开始!


重塑形状
X = x_train.reshape( x_train.shape[0], -1 ).T
X.shape
(784, 60000)


抛开样本数的维度,我们目标是把 维的“宽度和高度”重塑成 维。注意 reshape 函数第二个参数用到了 -1,这是我们偷懒不想计算 28×28。本来重塑后的形状是 (60000, 784),转置之后 形状是 (784, 60000)。转置的原因是有时我们会把点乘写成矩阵相乘的形式,而不想看到 XW这样的形式,而想看到 WX 这样的形式 (只是一种追求美感的惯例)


这时候上述公式进展到



张量点乘

初始化权重矩阵 和偏置向量 b。根据 的形状为 (784, 60000),可先推出 的形状应该为 (?,784);又根据输出 包含 10 个数字的概率值,因此 的形状为 (10, 1),则完全推出 的形状 (10, 784)


W = np.random.normal( 0, 1, size=(n_y, X.shape[0]) )
b = np.random.normal( 0, 1, size=(n_y, 1) )
print( W.shape )
print( b.shape )
(10, 784)
(10, 1)

利用 np.dot 计算 和 X 点乘得到 WX


WX = np.dot(W,X)
WX.shape
(1060000)

这时候上述公式进展到


广播机制
Z = WX + b
Z.shape
(10, 60000)


WX 的形状是 (10, 60000),b 的形状是 (10, 1),广播机制完全适用于两者的相加。这时候上述公式进展到


元素层面

最后用 tensorflow 里面的 tf.nn.softmax 来把 Z 转成 Y的形状是 (10,60000),显然 softmax 应该作用在每行上 (axis= 0),因此对于每一张图片 (一共 60000 ),输出应该是数字 到 10 对应的概率。


tensor_Y = tf.nn.softmax( Z, axis=0 )
Y = keras.backend.eval(tensor_Y)
Y.shape
(10, 60000)


tf.nn.softmax 输出来是 tensorflow 里面的 Tensor 变量,需要用 eval 方法把它转成 numpy 的形式。最后发现 和 的形状都是 (10, 60000)


最后来验证一下,是否 softmax 使用在行上了。如果是的,沿着每行加总应该等于 1 (概率之和要等于 1)


np.sum( Y, axis=0 )
array([1., 1., 1., ..., 1., 1., 1.])


这时候上述公式进展到




6总结


张量就是高维数组,数据无所不在,维度无所不在,因此张量无所不在。


深度学习里的神经网络可以看成是一组张量运算,谷歌下面著名的深度学习框架 TensorFlow 是建立了计算图,让张量 (tensor) 从中流动 (flow):因此得名 TensorFlow


张量之间的运算可归纳成四种,重塑、点乘、广播和元素层面。下面公式只是反映一层神经网络的张量流动,多层神经网络无非就是多次用到下面公式 (会有微小调整)。弄清楚每种张量运算的类型和运算前后的张量形状的匹配是最重要的。

细心的读者终于受不了了,想知道 和 怎么来的。答案:


  1. 用上面公式 遍得到输出 Y,和真实标签构建误差函数

  2. 对误差函数求所有 和 的导数

  3. 用各种各样的优化方法迭代求解


但是这里 和 都是矩阵或向量,而且深层神经网络又有很多个 和 b,那么在优化求解中,两个问题最重要


  1. 怎样有效的推导出误差函数对所有函数的偏导数?

  2. 怎样有效的计算它们?


解决问题 需要了解张量求导,解决问题 II 需要了解计算图 (反向传播)。这是下帖的内容,非常重要又十分有趣,学会了它们你不再是调包侠,学会了它们出了错你知道哪里去找,学会了它们你可以肆意推导 DNN, CNN, RNN (LSTM, GRU) 的反向传播公式。我的任务就是让你们轻松学会它们!Stay Tuned!



按二维码关注王的机器

迟早精通机学金工量投

年女儿生日给她写的信

2015 年

2016 年

2017 年

2018 年

2019 年

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存